package com.gitee.wsl.doc.prosemirror.state.selection

import com.gitee.wsl.doc.prosemirror.exception.RangeError
import com.gitee.wsl.doc.prosemirror.model.node.Node
import com.gitee.wsl.doc.prosemirror.model.content.ResolvedPos
import com.gitee.wsl.doc.prosemirror.model.fragment.Slice
import com.gitee.wsl.doc.prosemirror.state.Transaction
import com.gitee.wsl.doc.prosemirror.state.bookmark.SelectionBookmark
import com.gitee.wsl.doc.prosemirror.transform.ext.deleteRange
import com.gitee.wsl.doc.prosemirror.transform.ext.replaceRange
import com.gitee.wsl.doc.prosemirror.transform.ext.replaceRangeWith
import com.gitee.wsl.doc.prosemirror.transform.map.Mappable
import com.gitee.wsl.doc.prosemirror.transform.map.PositionRange
import com.gitee.wsl.doc.prosemirror.transform.step.ReplaceAroundStep
import com.gitee.wsl.doc.prosemirror.transform.step.ReplaceStep
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive

val classesById = mutableMapOf<String, SelectionClass>()

interface SelectionClass {
    fun fromJSON(doc: Node, json: JsonObject): Selection
}

// Superclass for editor selections. Every selection type should extend this. Should not be
// instantiated directly.
abstract class Selection(
    // Initialize a selection with the head and anchor and ranges. If no ranges are given,
    // constructs a single range across `$anchor` and `$head`.

    // The resolved anchor of the selection (the side that stays in place when the selection is
    // modified).
    val _anchor: ResolvedPos,
    // The resolved head of the selection (the side that moves when the selection is modified).
    val _head: ResolvedPos,
    // Actions (i.e. open links) will only trigger if enabled. You likely don't want this unless the user is actively
    // tapping to set this selection
    val allowActions: Boolean = false,
    ranges: List<SelectionRange>? = null
) {
    // The ranges covered by the selection.
    val ranges: List<SelectionRange> = ranges ?: listOf(SelectionRange(_anchor.min(_head), _anchor.max(_head)))

    // The selection's anchor, as an unresolved position.
    val anchor: Int
        get() = _anchor.pos

    // The selection's head.
    val head: Int
        get() = _head.pos

    // The lower bound of the selection's main range.
    val from: Int
        get() = _from.pos

    // The upper bound of the selection's main range.
    val to: Int
        get() = _to.pos

    val posRange: Pair<Int,Int>
        get() = from to to

    // The resolved lower  bound of the selection's main range.
    val _from: ResolvedPos
        get() = this.ranges[0].from

    // The resolved upper bound of the selection's main range.
    val _to: ResolvedPos
        get() = this.ranges[0].to

    // Indicates whether the selection contains any content.
    val isEmpty: Boolean
        get() = this.ranges.firstOrNull { it.from.pos != it.to.pos } == null

    // Get a [bookmark](#state.SelectionBookmark) for this selection, which is a value that can be
    // mapped without having access to a current document, and later resolved to a real selection
    // for a given document again. (This is used mostly by the history to track and restore old
    // selections.) The default implementation of this method just converts the selection to a text
    // selection and returns the bookmark for that.
    open fun getBookmark(): SelectionBookmark {
        return TextSelection.between(this._anchor, this._head).getBookmark()
    }

//    // Test whether the selection is the same as another selection.
//    abstract eq(selection: Selection): boolean

    // Map this selection through a [mappable](#transform.Mappable)
    // thing. `doc` should be the new document to which we are mapping.
    abstract fun map(doc: Node, mapping: Mappable): Selection

    // Get the content of this selection as a slice.
    open fun content(): Slice {
        return this._from.doc.slice(this.from, this.to, true)
    }

    // Replace the selection with a slice or, if no slice is given, delete the selection. Will
    // append to the given transaction.
    open fun replace(tr: Transaction, content: Slice = Slice.empty) {
        // Put the new selection at the position after the inserted
        // content. When that ended in an inline node, search backwards,
        // to get the position after that node. If not, search forward.
        var lastNode: Node? = content.content.lastChild
        var lastParent: Node? = null
        for (i in 0 until content.openEnd) {
            lastParent = lastNode!!
            lastNode = lastNode.lastChild
        }

        val mapFrom = tr.steps.size
        val ranges = this.ranges
        for (i in 0 until ranges.size) {
            val (_from, _to) = ranges[i].run { _from to _to }
            val mapping = tr.mapping.slice(mapFrom)
            tr.replaceRange(mapping.map(_from.pos), mapping.map(_to.pos), if (i != 0) Slice.empty else content)
            if (i == 0) {
                tr.selectionToInsertionEnd(
                    mapFrom,
                    if (lastNode?.isInline ?: (lastParent?.isTextblock != null)) -1 else 1
                )
            }
        }
    }

    // Replace the selection with the given node, appending the changes to the given transaction.
    fun replaceWith(tr: Transaction, node: Node) {
        val mapFrom = tr.steps.size
        val ranges = this.ranges
        for (i in ranges.indices) {
            val (_from, _to) = ranges[i].run { from to to }
            val mapping = tr.mapping.slice(mapFrom)
            val from = mapping.map(_from.pos)
            val to = mapping.map(_to.pos)
            if (i != 0) {
                tr.deleteRange(from, to)
            } else {
                tr.replaceRangeWith(from, to, node)
                tr.selectionToInsertionEnd( mapFrom, if (node.isInline) -1 else 1)
            }
        }
    }

    // Convert the selection to a JSON representation. When implementing this for a custom selection
    // class, make sure to give the object a `type` property whose value matches the ID under which
    // you [registered](#state.Selection^jsonID) your class.
    abstract fun toJSON(): JsonObject

    // Controls whether, when a selection of this type is active in the browser, the selected range
    // should be visible to the user. Defaults to `true`.
    val visible: Boolean = true

    companion object {
        // Find a valid cursor or leaf node selection starting at the given position and searching
        // back if `dir` is negative, and forward if positive. When `textOnly` is true, only
        // consider cursor selections. Will return null when no valid selection position is found.
        fun findFrom(_pos: ResolvedPos, dir: Int, textOnly: Boolean = false): Selection? {
            val inner = if (_pos.parent.inlineContent) {
                TextSelection(_pos)
            } else {
                _pos.node(0).findSelectionIn( _pos.parent, _pos.pos, _pos.index(), dir, textOnly)
            }
            if (inner != null) return inner

            for (depth in _pos.depth - 1 downTo 0) {
                val found =
                    if (dir < 0) {
                        _pos.node(0).findSelectionIn(
                            _pos.node(depth),
                            _pos.before(depth + 1),
                            _pos.index(depth),
                            dir,
                            textOnly
                        )
                    } else {
                        _pos.node(0).findSelectionIn(
                            _pos.node(depth),
                            _pos.after(depth + 1),
                            _pos.index(depth) + 1,
                            dir,
                            textOnly
                        )
                    }
                if (found != null) return found
            }
            return null
        }

        // Find a valid cursor or leaf node selection near the given position. Searches forward
        // first by default, but if `bias` is negative, it will search backwards first.
        fun near(_pos: ResolvedPos, bias: Int = 1): Selection {
            return this.findFrom(_pos, bias) ?: this.findFrom(_pos, -bias) ?: AllSelection(_pos.node(0))
        }

        // Find the cursor or leaf node selection closest to the start of the given document. Will
        // return an [`AllSelection`](#state.AllSelection) if no valid position exists.
        fun atStart(doc: Node): Selection {
            return doc.findSelectionIn( doc, 0, 0, 1) ?: AllSelection(doc)
        }

        // Find the cursor or leaf node selection closest to the end of the given document.
        fun atEnd(doc: Node): Selection {
            return doc.findSelectionIn( doc, doc.content.size, doc.childCount, -1) ?: AllSelection(doc)
        }

        // Deserialize the JSON representation of a selection. Must be implemented for custom
        // classes (as a static class method).
        fun fromJSON(doc: Node, json: JsonObject?): Selection {
            if (json == null || json["type"] == null) throw RangeError("Invalid input for Selection.fromJSON")
            val type = json["type"]!!.jsonPrimitive.content
            val cls = classesById[type] ?: throw RangeError("No selection type $type defined")
            return cls.fromJSON(doc, json)
        }

        // To be able to deserialize selections from JSON, custom selection classes must register
        // themselves with an ID string, so that they can be disambiguated. Try to pick something
        // that's unlikely to clash with classes from other modules.
        fun jsonID(id: String, selectionClass: SelectionClass): SelectionClass {
            if (id in classesById) throw RangeError("Duplicate use of selection JSON ID $id")
            classesById[id] = selectionClass
            return selectionClass
        }
    }
}

var warnedAboutTextSelection = false
fun ResolvedPos.checkTextSelection( ) {
    if (!warnedAboutTextSelection && ! parent.inlineContent) {
        warnedAboutTextSelection = true
//        Log.w("TextSelection", "endpoint not pointing into a node with inline content (${_pos.parent.type.name})")
    }
}

// Try to find a selection inside the given node. `pos` points at the position where the search
// starts. When `text` is true, only return text selections.
fun Node.findSelectionIn( node: Node, pos: Int, index: Int, dir: Int, text: Boolean = false): Selection? {
    val doc = this
    var pos = pos
    if (node.inlineContent) return TextSelection.create(doc, pos)
    var i = index - (if (dir > 0) 0 else 1)
    while (if (dir > 0) i < node.childCount else i >= 0) {
        val child = node.child(i)
        if (!child.isAtom) {
            val inner = doc.findSelectionIn( child, pos + dir, if (dir < 0) child.childCount else 0, dir, text)
            if (inner != null) return inner
        } else if (!text && NodeSelection.isSelectable(child)) {
            return NodeSelection.create(doc, pos - (if (dir < 0) child.nodeSize else 0))
        }
        pos += child.nodeSize * dir
        i += dir
    }
    return null
}

fun Transaction.selectionToInsertionEnd(startLen: Int, bias: Int) {
    val last = steps.size - 1
    if (last < startLen) return
    val step = steps[last]
    if (!(step is ReplaceStep || step is ReplaceAroundStep)) return
    val map = mapping.maps[last]
    var end: Int? = null
    map.forEach { _, _, _, newTo -> if (end == null) end = newTo }
    setSelection(Selection.near(doc.resolve(end!!), bias))
}
